package com.google.code;

import static com.google.code.BunitUtils.isChildOf;
import static org.junit.runner.Description.createSuiteDescription;
import static org.junit.runner.Description.createTestDescription;

import java.lang.reflect.Field;
import java.util.Iterator;

import org.junit.runner.Description;
import org.junit.runner.Runner;
import org.junit.runner.notification.Failure;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.TestClass;

/**
 * @author Leopoldo Muller
 */
public class Bunit extends Runner {

    /**
     * Test class to reflect for fields and methods
     */
    private final TestClass testClass;

    /**
     * JUnit test description to inform steps, progress and results
     */
    private final Description description;

    /**
     * Instance of bean factory for corresponding test case
     */
    private final BunitContext context;

    /**
     * Default runner constructor.
     * 
     * @param klass
     *            test class
     */
    public Bunit(Class<?> klass) {

        context = new BunitContext(klass);
        testClass = new TestClass(klass);
        description = createDescription();
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.junit.runner.Runner#getDescription()
     */
    @Override
    public Description getDescription() {

        return description;
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.junit.runner.Runner#run(org.junit.runner.notification.RunNotifier)
     */
    @Override
    public void run(RunNotifier notifier) {
        // test class
        Class<?> javaClass = testClass.getJavaClass();
        // test steps
        Iterator<Description> children = description.getChildren().iterator();
        // current step
        Description step = children.next();
        // exception thrown by when method
        Throwable whenException;
        try {
            // inject given attributes
            for (Field f : javaClass.getDeclaredFields()) {

                if (f.isAnnotationPresent(Given.class)) {

                    notifier.fireTestStarted(step);
                    // injectGiven(f);
                    notifier.fireTestFinished(step);
                    step = children.next();
                }
            }
            // invoke when method
            notifier.fireTestStarted(step);
            // hold when step description to inform unhandled exception if needed
            Description whenStep = step;

            try {

                invokeWhen();

                whenException = null;

            } catch (Throwable t) {
                // hold exception if occurs
                whenException = t;
            }

            notifier.fireTestFinished(step);
            step = children.next();
            // invoke then methods
            for (FrameworkMethod method : testClass.getAnnotatedMethods(Then.class)) {

                notifier.fireTestStarted(step);

                if (whenException != null && isThenShouldThrows(method)) {

                    invokeThenShouldThrows(method, whenException);
                    // exception handled
                    whenException = null;

                } else {

                    invokeThen(method);
                }

                notifier.fireTestFinished(step);

                if (children.hasNext()) {

                    step = children.next();
                }
            }
            // exception not handled
            if (whenException != null) {

                notifier.fireTestFailure(new Failure(whenStep, whenException));
            }
        } catch (Throwable t) {

            notifier.fireTestFailure(new Failure(step, t));
        }
    }

    /**
     * Invoke the <i>Then Should Throws</i> method passing an exception as parameter.
     * 
     * @param method
     *            the {@link Then} method to invoke in test instance
     * 
     * @param throwable
     *            the exception to be passed as parameter
     * 
     * @throws Throwable
     *             if {@link FrameworkMethod#invokeExplosively(Object, Object...)} fail
     */
    void invokeThenShouldThrows(FrameworkMethod method, Throwable throwable) throws Throwable {

        invokeThen(method, throwable);
    }

    /**
     * Invoke a <i>Then Method</i> using the bean-factory bean as argument if exists.
     * 
     * @param method
     *            the {@link Then} method to invoke in test instance
     * 
     * @throws Throwable
     *             if {@link FrameworkMethod#invokeExplosively(Object, Object...)} fail
     */
    void invokeThen(FrameworkMethod method) throws Throwable {

        invokeThen(method, context.get(method.getAnnotation(Then.class).value()));
    }

    /**
     * Invoke a <i>Then Method</i> with one optional {@code param} and check if the response is {@code true}, otherwise
     * throw an {@link AssertionError}.
     * 
     * @param method
     *            the {@link Then} method to invoke in test instance
     * 
     * @param param
     *            one optional parameter to pass as argument to <i>Then Method</i>
     * 
     * @throws Throwable
     *             if {@link FrameworkMethod#invokeExplosively(Object, Object...)} fail
     */
    void invokeThen(FrameworkMethod method, Object param) throws Throwable {

        if (!((Boolean) invoke(method, param))) {

            throw new AssertionError(method.getAnnotation(Then.class).value());
        }
    }

    /**
     * Utility method to invoke methods with one optional parameter.
     * 
     * @param method
     *            the method to invoke in test instance
     * 
     * @param optionalParameter
     *            parameter to pass as argument to method if not null. if null invoke no-argument method
     * 
     * @return whatever the {@code method} returns
     * 
     * @throws Throwable
     *             if {@link FrameworkMethod#invokeExplosively(Object, Object...)} fail
     */
    Object invoke(FrameworkMethod method, Object optionalParameter) throws Throwable {

        Object result;
        Object test = context.getTestInstance();

        if (optionalParameter == null) {

            result = method.invokeExplosively(test);

        } else {

            result = method.invokeExplosively(test, optionalParameter);
        }

        return result;
    }

    /**
     * Invoke the <i>When Method</i> using the bean-factory bean as argument if exists.
     * 
     * @throws Throwable
     *             if {@link FrameworkMethod#invokeExplosively(Object, Object...)} fail
     */
    void invokeWhen() throws Throwable {

        FrameworkMethod method = testClass.getAnnotatedMethods(When.class).get(0);

        invoke(method, context.get(method.getAnnotation(When.class).value()));
    }

    /**
     * Check if is a <i>Then Should Throws</i> method. <i>Then Should Throws</i> methods are identified by receiving a
     * subclass of {@link Throwable} as the unique parameter.
     * 
     * @param method
     *            method to check if is a <i>Then Should Throws</i> method
     * 
     * @return {@code true} if is and <i>Then Should Throws</i> method or {@code false} if not.
     */
    boolean isThenShouldThrows(FrameworkMethod method) {

        Class<?>[] parameterTypes = method.getMethod().getParameterTypes();

        return parameterTypes.length == 1 && isChildOf(parameterTypes[0], Throwable.class);
    }

    /**
     * Create the JUnit test description for Bunit scenario. The description will be like:
     * 
     * given ... and ... and ... when ... then ... and ... and ... .
     * 
     * @return
     */
    Description createDescription() {

        Class<?> javaClass = testClass.getJavaClass();

        FrameworkMethod when = testClass.getAnnotatedMethods(When.class).get(0);

        Description desc = createSuiteDescription(javaClass);

        String detail = "given ";

        for (Field f : javaClass.getDeclaredFields()) {

            if (f.isAnnotationPresent(Given.class)) {

                desc.addChild(createTestDescription(javaClass, detail + f.getAnnotation(Given.class).value()));

                detail = "and ";
            }
        }

        detail = "when ";

        desc.addChild(createTestDescription(javaClass, "when " + when.getAnnotation(When.class).value()));

        detail = "then ";

        for (FrameworkMethod fm : testClass.getAnnotatedMethods(Then.class)) {

            desc.addChild(createTestDescription(javaClass, detail + fm.getAnnotation(Then.class).value()));
            detail = "and ";
        }

        return desc;
    }
}
